module IPFS.Client.Utils.SendRequest (
  sendRequest,
  closeResponse,
  resp2SyncData,
  RespData(..),
  RespSyncData(..),
  requestAPI,
  execCommand,
  execJsonCommand,
  execRawJsonCommand,
  execEffectStreamBuffer,
  execEffectCommand,
  execBufferCommand,
  execStreamBufferCommand,
  execAffStreamJsonCommand,
  execEffectStreamJsonCommand,
  execEffectStreamDecodeJsonCommand
) where

import Prelude

import Data.Argonaut (class DecodeJson, Json, decodeJson, fromArray, fromString, jsonParser, stringify)
import Data.Either (Either(..))
import Data.Maybe (Maybe(..), fromJust, isJust, maybe)
import Data.String.CodeUnits (length)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff (Aff, Canceler(..), Error, makeAff)
import Effect.Class.Console (logShow)
import Foreign.Object as O
import IPFS.Client as C
import IPFS.Client.Command (Command)
import IPFS.Multiaddr.IP as IP
import Network.HTTP as HTTP
import Network.HTTP.QueryString (ArrayPrefixGenerator(..))
import Network.HTTP.QueryString as QS
import Node.Buffer (Buffer, toString)
import Node.Encoding (Encoding(..))
import Node.SteamEx.Readable as SR
import Node.Stream (end, onEnd, onError)
import Node.Stream.Helper as SH
import Node.Stream.Helper.NdJson as ND
import Node.Stream.Helper.Split2 as SP
import Node.StreamEx.Stream (fromDuplex, fromReadableStream)
import Node.StreamEx.Types as ST
import Node.URL as URL
import Partial.Unsafe (unsafePartial)
import Unsafe.Coerce (unsafeCoerce)

data RespData  =  RespBuffer Buffer | RespJson Json |  RespStreamJson (ST.Readable Json) | RespStreamBuffer (ST.Readable Buffer)

data RespSyncData = RespSyncBuffer Buffer | RespSyncJson Json

instance showRespSyncData :: Show RespSyncData where
  show (RespSyncBuffer buf)    = "(RespSyncBuffer " <> show buf  <> ")"
  show (RespSyncJson  json)    = "(RespSyncJson "   <> stringify json  <> ")"

resp2SyncData::RespData -> (Either String RespSyncData -> Effect Unit) -> Effect Unit
resp2SyncData (RespBuffer buf) cb        = cb $ Right $ RespSyncBuffer buf
resp2SyncData (RespJson json)  cb        = cb $ Right $ RespSyncJson json
resp2SyncData (RespStreamJson  stream) cb = do
  write::ST.Writable Json <- SH.concatEX (\jsonArr -> cb $ Right $ RespSyncJson $ fromArray jsonArr)
  SR.pipe stream write Nothing
resp2SyncData (RespStreamBuffer  bufStream )  cb  = do 
  write::ST.Writable Buffer <- SH.concatEX (\buf -> cb $ Right $ RespSyncJson buf)
  SR.pipe bufStream write Nothing

instance showRespData :: Show RespData where
  show (RespBuffer buf)      = "(RespBuffer " <> show buf  <> ")"
  show (RespJson  json)      = "(RespJson "   <> stringify json  <> ")"
  show (RespStreamJson  _)    = "(RespStreamJson )"
  show (RespStreamBuffer  _)  = "(RespStreamBuffer )"

mapRight::forall a b c. Either a b -> (b -> (Either a c)) ->  Either a c
mapRight  (Left  v) _  = Left v
mapRight  (Right v) fn = fn v

matchRawRespJson :: RespData -> Either String Json
matchRawRespJson (RespJson json) = Right json
matchRawRespJson _               = Left "not json format"

matchStreamBuffer :: RespData -> Either String (ST.Readable Buffer)
matchStreamBuffer (RespStreamBuffer  bufStream)  = Right $ bufStream
matchStreamBuffer _                              = Left "not StreamBuffer format"

matchBuffer :: RespData -> Either String Buffer
matchBuffer (RespBuffer buf ) = Right buf
matchBuffer _                 = Left "not buffer format"

matchStreamJson :: RespData -> Either String (ST.Readable Json)
matchStreamJson (RespStreamJson  streamJson) = Right $ streamJson
matchStreamJson _                            = Left "not stream json format"
-----------------------------------exec aff command--------------------------------------

execRawJsonCommand::C.Client -> Command -> Aff (Either String Json)
execRawJsonCommand = execSwitchRespCommand matchRawRespJson

execJsonCommand::forall a.DecodeJson a => C.Client -> Command -> Aff (Either String a)
execJsonCommand client cmd = (execRawJsonCommand client cmd) >>= (\x -> pure $ join $ decodeJson <$> x)

execStreamBufferCommand ::C.Client -> Command -> Aff (Either String (ST.Readable Buffer))
execStreamBufferCommand = execSwitchRespCommand matchStreamBuffer

execBufferCommand::C.Client -> Command -> Aff (Either String Buffer)
execBufferCommand = execSwitchRespCommand matchBuffer

execAffStreamJsonCommand::C.Client -> Command -> Aff (Either String (ST.Readable Json))
execAffStreamJsonCommand = execSwitchRespCommand matchStreamJson

execSwitchRespCommand::forall a. (RespData -> Either String a) -> C.Client -> Command -> Aff (Either String a)
execSwitchRespCommand fn client cmd  = do
  resp <- execCommand client cmd
  pure $ mapRight resp fn
-----------------------------------exec effect command--------------------------------------

execEffectStreamJsonCommand::C.Client -> Command -> (HTTP.Response -> Effect Unit) -> (Either String Json -> Effect Unit) -> Effect HTTP.Request
execEffectStreamJsonCommand client cmd startCallback callback = do
  execEffectCommand client cmd startCallback (\eData -> case  eData of
                                                Left l  -> callback $ Left l
                                                Right r -> case r of
                                                       (RespStreamJson  stream) -> ND.onJsonEither (unsafeCoerce stream) callback
                                                       otherwise               -> callback $ Left "not stream json format"
                                                        
                                             )

execEffectStreamDecodeJsonCommand::forall a.DecodeJson a => C.Client -> Command -> (HTTP.Response -> Effect Unit) 
                                                            -> (Either String  a -> Effect Unit) -> Effect HTTP.Request
execEffectStreamDecodeJsonCommand  client cmd startCallback callback = execEffectStreamJsonCommand client cmd startCallback (\e -> callback $ join $ decodeJson <$> e)

execEffectStreamBuffer::C.Client -> Command -> (HTTP.Response -> Effect Unit) -> (Either String (ST.Readable Buffer) -> Effect Unit) -> Effect HTTP.Request
execEffectStreamBuffer client cmd startCallback callback = execEffectCommand client cmd startCallback (\e -> callback $ mapRight e matchStreamBuffer)

execCommand::C.Client -> Command -> Aff (Either String RespData)
execCommand client cmd = sendRequest client cmd 

execEffectCommand::C.Client -> Command -> (HTTP.Response -> Effect Unit) -> (Either String RespData -> Effect Unit) -> Effect HTTP.Request
execEffectCommand client cmd startCallback callback = do
   requestAPI client cmd startCallback (\eData -> case eData of 
                                          Left err -> callback $ Left (show err)
                                          Right r  -> callback r
                                        )

sendRequest::C.Client -> Command -> Aff (Either String RespData)
sendRequest client cmd = do
  makeAff runRequest
 where
   runRequest::(Either Error (Either String RespData) -> Effect Unit) -> Effect Canceler
   runRequest ef = do
     _ <- requestAPI client cmd (\_-> pure unit) ef
     pure $ Canceler (\e -> pure unit)

requestAPI::C.Client ->  Command -> (HTTP.Response -> Effect Unit) -> (Either Error (Either String RespData) -> Effect Unit) -> Effect HTTP.Request
requestAPI (C.Client client) cmd startCallback callback = do
     req <- HTTP.request reqOptions (onRes startCallback cmd callback)
     let reqWriter = HTTP.requestAsStream req
     onError reqWriter \err -> callback $ Right $ Left $ "requestAPI OnError:" <> show err
     if (not $ isMaybeTrue cmd.stream) then end reqWriter (pure unit) else pure unit
     pure req
 where
  url = client.protocol <> "://" <> client.host <> ":" <> IP.toString client.port <> "/" <> client.apiPath <> cmd.path <> qsString
  reqOptions = { reqURL:URL.parse url,reqMethod:HTTP.POST,reqHeader:headers}
  headers = if  isMaybeTrue cmd.multipart then O.fromFoldable ["Content-Type" /\ 
                                                              ("multipart/form-data;boundary=" <> (unsafePartial $ fromJust cmd.multipartBoundary))] 
                                          else O.empty
  isMaybeTrue mbool = maybe false identity mbool
  mqs = case cmd.args of
                 Nothing -> cmd.qs
                 Just j  -> O.union cmd.qs $ O.fromFoldable ["arg" /\ (fromArray j)]
  qsString = QS.stringify mqs (QS.defaultStringifyOption {generateArrayPrefix = Repeat})

onRes::(HTTP.Response -> Effect Unit) -> Command -> (Either Error (Either String RespData) -> Effect Unit) -> HTTP.Response -> Effect Unit
onRes startCallback cmd callback resp = do
    _ <- startCallback resp
    onResMIF
 where
  readerW = HTTP.responseAsStream  resp
  header = HTTP.responseHeaders resp
  isStream = isJust $ O.lookup "x-stream-output" header
  isChunkedObject = isJust $ O.lookup "x-chunked-output" header
  isJson = maybe false identity $ O.lookup "content-type" header >>= (Just <<< eq "application/json")
  code = HTTP.statusCode resp
  onResMIF::Effect Unit
  onResMIF  | code >= 400 = do
                            callback $ Right $ Left $ "Server responded with " <> show code
            | isChunkedObject && isJson && (not cmd.disableChunkJson) = do
                                    outputStream <- SP.split2 
                                    _ <-SH.pump readerW outputStream (\err -> callback $ Right $ Left err)
                                    onEnd readerW (do
                                                    logShow "Stream Json End"
                                                    pure unit
                                                 )
                                    let (readable::ST.Readable Json) = ST.toReadable $ fromDuplex outputStream::ST.Duplex Json
                                    callback $ Right $ Right $ RespStreamJson  readable
                                    pure unit
            | isStream && (not cmd.isBuffer)  =  do
                                                  let (readable::ST.Readable Buffer) = ST.toReadable $ fromReadableStream readerW::ST.Readable Buffer
                                                  callback $ Right $ Right $ RespStreamBuffer  readable
            | isJson     = do
                            concatW <- SH.concat concatFunc
                            _ <- SH.pump readerW concatW　(\err -> callback $ Right $ Left err)
                            pure unit
            | otherwise  = do
                            concatW <- SH.concat (\buf -> callback $ Right $ Right $ RespBuffer buf)
                            _ <- SH.pump readerW concatW　(\err -> callback $  Right $ Left err)
                            pure unit
  concatFunc  buf = do
     str <- toString UTF8 buf
     if (eq (length str) 0) then (callback $ Right $ Right $ RespJson $ fromString "") else (callback $ Right $ RespJson <$> (jsonParser str))
     pure unit

foreign import closeResponse::HTTP.Response -> Effect Unit